[Previous] [Next]

The DHTMLPage Designer

The big news about Visual Basic 6 is that you can write DHTML code using your favorite language, thanks to the DHTMLPage designer. Like all designers, the DHTMLPage designer exposes a visual part (the HTML page) and a code section. When you compile the program, you produce an ActiveX DLL that runs inside Internet Explorer 4.01 or later versions. Being able to access the DHTML object from a compiled DLL written in Visual Basic has a lot of advantages:

A First Look at the DHTMLPage Designer

The fastest way to show you the DHTMLPage designer in action is to select the DHTML Application template from the project gallery. This template adds one instance of the DHTMLPage designer and one standard BAS module, which contains some useful routines. In a typical DHTML application you'll create several DHTMLPage designers, one for each DHTML page your program consists of.

Figure 19-6 shows the DHTMLPage designer, with a treeview pane on the left and a detail pane on the right. The two panes are actually different representations of the contents of the page: In the treeview pane you see the hierarchical relationships of the elements of the page, in the detail pane you see (and arrange) the elements as if they were controls on a form. The designer doesn't provide access to the HTML code behind the page, so you can't add script routines or HTML tags directly. Fortunately, you don't really need to use scripts any longer because you'll be using Visual Basic, and you can use an external editor to author an HTML page and then import it into the designer.

Click to view at full size.

Figure 19-6. The DHTMLPage designer.

When the designer is active, a new tab appears in the Toolbox window, containing all the HTML controls that you can drop in the detail pane. All the controls we've seen so far are included in the toolbox, plus a few new ones: hidden TextBox controls, InputImage controls, FileUpload controls. To simplify the developer's job, you have distinct icons for the one-line Select control and for the multiline (and optionally multiselect) List control, even though they're rendered using the same HTML tags. The toolbox also contains a few items which aren't controls in the stricter sense of the word: the HorizontalRule element (for drawing horizontal lines) and Hyperlink element. You can also create a Hyperlink by selecting a portion of text and clicking on the Make Selection Into Link button on the toolbar. Moreover, if you write text that's formatted as a Web address (such as www.microsoft.com) the designer automatically turns it into a hyperlink.

As you know, an HTML page can contain ActiveX controls, and the DHTMLPage designer supports this capability too. You can drop an external ActiveX control on the page, such as a TreeView or an ActiveX control you've authored in Visual Basic and compiled as a stand-alone OCX file. You can't use intrinsic Visual Basic controls, nor can you use UserControl objects that are private in the current project.

The topmost portion of the DHTMLPage designer toolbar (visible in Figure 19-7) gives you the ability to format the text or the element currently selected. The second combo box from the left is filled with all the styles defined for the current page, including those defined in an external Cascading Style Sheet that the page refers to. Because you can't define a style in the DHTMLPage designer, this combo box can contain elements only if you've imported an external HTML page written with a more powerful editor.

Click to view at full size.

Figure 19-7. The toolbar of the DHTMLPage designer.

Using the DHTMLPage Designer Properties button, you can decide whether the HTML page being built should be saved together with the current project or as a separate HTM file. Each choice has its advantages, but because the designer can't compete with more powerful HTML editors, such as Microsoft FrontPage, I suggest that you use the latter option so that you can use an external HTML editor to embellish the page.

The Launch Editor button lets you edit the current page using the external editor of your choice. By default, this editor is Notepad, which hardly qualifies as an HTML editor but is used by many HTML programmers nonetheless. You can define a better editor in the Advanced tab of the Options dialog of the Visual Basic IDE. You can edit the page in an external editor only if you saved it as an external HTM file. When you click on this button, Visual Basic automatically saves the project with the most recent edits, and then runs the external editor. Visual Basic continuously checks the file's date and time, and as soon as you save the page in the editor Visual Basic asks whether you want to reload it in the DHTMLPage designer.

As with all designers, you can click on a control (in either of the two panes) and then press the F4 key to bring up the Properties window. In the DHTMLPage designer, you can modify the attributes of any element, including plain text. I suggest that you create an empty HTML page and drop an instance of each control in the toolbox and then press F4 to become familiar with the properties it exposes. In the Properties window, you can read the type of each element, according to the name classification used by the designer. For example, many of the controls dropped from the toolbox are of type DispIHTMLInputElement and are further classified by their type property (which can be text, password, image, and so on). The class of Hyperlink elements is DispHTMLAnchorElement. Tables are of class DispHTMLTable, and they contain elements whose class is DispHTMLTableCell.

Speaking of tables, you have a lot of options when creating and editing tables, for example using the drop-down menu from the designer's toolbar or by right-clicking on the table itself in the detail pane. The pop-up menu includes the Properties command, which brings up the Properties dialog box shown in Figure 19-8. In this dialog box, you can set a lot of attributes, and you can also span cells to take multiple rows and columns. Also notice that a button on the toolbar lets you show and hide table borders at design time, without affecting the actual Border attribute. Having a table with visible borders at design time usually simplifies your editing chores.

Figure 19-8. The Properties dialog box of a DispHTMLTable object.

If you click on an external ActiveX control and press the F4 key, you get the list of supported HTML attributes, not the usual list of properties specific to that control. To edit the intrinsic properties of an external ActiveX control, you must right-click on it and bring up its Properties dialog box. The List and the Select DHTML controls support a custom Properties dialog box as well, so you can use it for specifying the list of items these controls contain.

The designer can work in two position modes, relative or absolute. In relative mode, you let the browser position all the elements in the page when the page itself is resized, as it happens for all the (non-Dynamic) HTML pages. In absolute mode, the element stays where you drop it. On the toolbar, two buttons affect position mode: One button affects the current mode, and the other affects the absolute position attribute of the element that's currently selected. The latter button is disabled when you select text elements because you can modify the position of a text element only by pressing the Enter key to add empty lines, as you do in a word processor. Because hyperlinks are just text elements, they're subject to the same rules for positioning. All the other elements can be moved with the mouse, but you have to grab them from their border. You can control the z-order position of page elements by using the Order submenu on the designer's toolbar.

Setting text properties in the internal designer isn't the most intuitive action. In fact, a <P> element doesn't initially expose any font or style property. To force it to expose such attributes, you must change its appearance using the topmost toolbar, for example, by modifying the font size. When you change the standard appearance of a paragraph, a <FONT> element appears in the treeview as a child of the <P> item. You can then select this new item and press the F4 to display the Properties window, and then change other attributes, such as color and face.

Programming DHTML Elements

To exploit the dynamic capabilities of DHTML, you must write code that reacts to events raised by the page or its elements. In a Visual Basic 6's DHTML application, you write code that reacts to events raised by the page and its elements, exactly as you write code behind the controls on a form. When you then compile the application, Visual Basic creates one or moreHTM files and a DLL that contains the compiled code you've written inside event procedures. This DLL will be loaded in the address space of Internet Explorer, and can trap DHTML events exposed to the outside by that browser.

All Visual Basic 6's DHTML applications are actually ActiveX DLL applications, whose threading model is apartment threading. You shouldn't use the DHTMLPage designer inside single-threaded ActiveX projects. All theHTM files produced when you compile the application contain an OBJECT tag with a reference to the corresponding DLL. The first time the user navigates to the page, the DLL is automatically downloaded from the server and installed in the client's system. This mechanism is identical to the one used for downloading an ActiveX control in an HTML page.

The DHTMLPage object

The DHTMLPage object represents the component in the DLL that's bound to a particular HTM page. Like all objects, it exposes an Initialize and a Terminate events, which fire the first time the page is used and immediately before the DLL is unloaded, respectively. It also exposes two events, Load and Unload, which fire when the page is loaded and unloaded, respectively.

The DHTMLPage object exposes four design-time properties. The SourceFile property is the path of theHTM file that contains the HTML source of the page being built or an empty string if you aren't editing the page using an external editor. The BuildFile property is the path of theHTM file that will be built during the compilation process and that should be distributed with the DLL. (It's initially the same value as the SourceFile property.) The AsynchLoad property specifies whether the page should be loaded asynchronously. (See more on this in the section "Loading a Page Asynchronously" later in this chapter.) Specifying the id property makes the page programmable. In the Properties window, you'll also find a fifth property, Public, but you can't actually count it because it's set to True and can't be changed. (You can't have private DHTMLPage objects.)

These properties are available only at design time. At run time, the DHTMLPage object exposes a different set of properties: BaseWindow, Document, and DHTMLEvent (as shown in Figure 19-9). They return a reference to the all-important DHTML Window, Document, and Event objects respectively, and so they're the links between the Visual Basic program and the Dynamic HTML object mode. Notice that while you can access DHTML objects from within the DHTMLPage designer module, you can't access the designer from a script inside the HTML page. The page is oblivious to the fact that it's being processed by a DLL.

Click to view at full size.

Figure 19-9. The runtime properties of the DHTMLPage object.

When inside a DHTML module, you can directly reference the DHTMLPage object's properties in code, exactly as you do with a form's properties inside a form code module. The following example demonstrates this concept:

' (This code must run inside a DHTMLPage code module.)
' Change the background color of the page.
Document.bgcolor = "red"
' Retrieve the state of the Alt key inside an event procedure.
If DHTMLEvent.altKey Then ...

The id property

Not all the page elements can be associated with event procedures. In order to be programmable, a page element must have a nonempty id property. This id becomes the name by which you refer to that element in code. This requirement can mislead Visual Basic programmers because many HTML elements also support a Name property, which is usually meaningless in pure HTML programming. As I explained in the HTML tutorial at the beginning of this chapter, the Name property is mostly used for grouping mutually exclusive Option controls. In DHTML applications, you need different id values even for the items in a group of Option controls if you want to refer to them individually.

Under standard DHTML, the id properties of multiple controls don't need to be different. Within a DHTML application written in Visual Basic, however, all the id's in a page must be unique. When you import an existing .hml file into the designer, Visual Basic checks all the id values, and if necessary, it automatically appends a number to them to ensure that their values are unique in the page. So always double-check the id assigned to an element when you import an HTM page.

Not all the elements in the page need to have an id property. In fact, in most cases they don't: Only the elements that you want to write code for have to be assigned an id. The treeview in the leftmost pane shows such programmable elements in boldface. The designer automatically creates an id for all the elements and controls that you drop from the Toolbox. To assign an id to an element, you select it in the treeview, switch to the Properties window, and then type a unique value for the id property. You can also select a page element in the combo box controls at the top of the Properties window. (Use this method to change the properties of the DHTMLPage object itself.) Moreover, you can always access the properties and methods of a page element through the All property and other collections of the Document object.

The first example: A dynamic menu

To show you how you can leverage what you learned about DHTML programming, let's build a practical example: a dynamic menu consisting of items that appear and disappear when you click on the menu header and that are rendered as bold text when the mouse passes over them.

To begin with, create a new DHTML page, save it in an HTM file, and then add a few paragraphs as shown in Figure 19-10. Set these paragraphs' id properties to MainMenu, MenuItem1, MenuItem2, and MenuItem3 respectively. You can change the color of each paragraph by changing its font size from the toolbar, and then editing the properties of the <FONT> item that the designer creates for you.

Click to view at full size.

Figure 19-10. The first DHTML application: a dynamic menu.

Now you can finally write code for managing these items. You write code behind a page element the same way you write code for a control on a regular form: You double-click on the element (in the treeview pane) to access the code window, and then select the event procedure in the rightmost combo box. You can also access the code window by pressing the F7 function key or by selecting the View Code command from the menu that appears when you right-click on the designer window. This is the code you should enter in the code module. (Or you can load the sample application from the companion CD.)

Private Sub DHTMLPage_Load()
    ' Make the submenu choices invisible when the page loads.
    SetVisibility False
End Sub

' Change the display attribute of all the menu items.
Private Sub SetVisibility(newValue As Boolean)
    MenuItem1.Style.display = IIf(newValue, "", "none")
    MenuItem2.Style.display = IIf(newValue, "", "none")
    MenuItem3.Style.display = IIf(newValue, "", "none")
End Sub 

' When the MainMenu paragraph is clicked, 
' switch menu items from hidden to visible and back.
Private Function MainMenu_onclick() As Boolean
    If MenuItem1.Style.visibility = "hidden" Then
        SetVisibility True
    Else
        SetVisibility False
    End If
End Function

' Change the boldface attribute of the element under the mouse, but only
' if this element is one of the three MenuItem paragraphs.
Private Sub Document_onmouseover()
    Select Case DHTMLEvent.srcElement.innerText
        Case "Click here", "Acknowledgments", "Table of contents", _
            "Appendix"
            DHTMLEvent.srcElement.Style.fontWeight = "800"
    End Select
End Sub

' Restore the original font attribute when the mouse leaves the element.
Private Sub Document_onmouseout()
    Select Case DHTMLEvent.srcElement.innerText
        Case "Click here", "Acknowledgments", "Table of contents", _
            "Appendix"
            DHTMLEvent.srcElement.Style.fontWeight = ""
    End Select
End Sub

In its simplicity, the preceding code is a good example of how you can put DHTML features to good use. Because all the menu items behave in a similar way, it doesn't make any sense to repeat the same code inside their onmouseover and onmouseout event procedures. In fact, it's much better to exploit the event bubbling features of Dynamic HTML and trap those events at the Document level. This is something that you couldn't do if this were a regular Visual Basic form.

This approach has its drawbacks, however, because you need to be sure that the onmouseover and onmouseout events were raised by one of the four <P> elements you're interested in, and not by something else on the page. The Event object exposes a srcElement property that returns a reference to the object that first originated the event. The problem is this: How can you determine whether this object is one of the four <P> items that make up the menu? At first I believed that I could compare the id properties of those four items with the value returned by the DHTMLEvent.srcElement.id property, but—to my surprise—I discovered that the latter property always returns an empty string and so can't be used for this purpose. Fortunately, you can solve the problem with the innerText property. If multiple elements on the page have the same value for the innerText property, you should assign them a unique Name and use this property to find out which element is raising the event.

Using DIV and SPAN tags

In most cases, you don't need to resort to the unusual technique based on id, innerText, or some other property to figure out whether you're interested in the event because under Dynamic HTML you can precisely delimit the range of event bubbling by creating an area of the document that exactly contains only the items you're interested in. If you don't have a container that holds all the elements you want receive events from (and only them), you can group the elements you're interested in using a <DIV> and </DIV> pair of tags.

For the dynamic menu example, you need to create a DIV section that comprises the four menu items. This is really simple: In the pane on the right side of the DHTMLPage designer, select the four paragraphs and then click on the third button from the left in the lower line of buttons in the designer's toolbar. This action creates a DIV section, but you need to assign it a nonempty id property to make it programmable. So type DynMenu in the Properties window, and then go to the code window to enter this code:

Private Sub DynMenu_onmouseover()
    DHTMLEvent.srcElement.Style.fontWeight = "800"
End Sub

Private Sub DynMenu_onmouseout()
    DHTMLEvent.srcElement.Style.fontWeight = ""
End Sub

As you see, you don't need to test the srcElement.innerText property because you're sure that the event comes from one of those four <P> items.

As an exercise, let's see how you use the <SPAN> tag, which is often useful for referencing smaller portions of the HTML page. Let's suppose that you want to change the text of the MainMenu element to Click here to close the menu when the menu is open, and you want to restore it to Click here when the menu is closed. One way to obtain this behavior is to extend the text of the MainMenu element to Click here to close the menu, select the last four words, and click the fourth button on the designer toolbar to turn this small portion of text into a <SPAN> section. To refer to this section from within code, you need to assign this <SPAN> object an id (CloseMenu, for example) and then update the code as follows. (Added statements are in boldface.)

Private Function MainMenu_onclick() As Boolean
    If MenuItem1.Style.visibility = "hidden" Then
        SetVisibility True
        MenuClose.Style.visibility = "visible"
    Else
        SetVisibility False
        MenuClose.Style.visibility = "hidden"
    End If
End Function

As you see, Dynamic HTML lets you achieve eye-catching results with a small amount of code.

DHTML event procedures

If you look carefully at the code in the sample application, you'll notice that many (but not all) event routines are functions, rather than procedures. As I explained in the "Canceling the Default Effect" section earlier in this chapter, all DHTML events expect a return value that, if False, cancels the default action for the event. In order to return a value, the event procedure must be declared as a function.

The way you return a value from an event inside a DHTMLPage designer is different from the technique used within script routines inside the HTM file. In VBScript, you must explicitly set the return value of a procedure to False to cancel the default action of a given event, or you must set the event.returnValue property to False to reach the same result. In Visual Basic, however, False is the default value for any Function, and DHTML event procedures are no exception to this rule. In other words, if you write an event procedure you must explicitly set its return value to True if you don't want to cancel the default action.

To explain this concept with an example, let's say that you have a hyperlink and you want to ask for a confirmation before letting the user navigate to the specified URL. This is the code you have to write in the Hyperlink object's onclick event procedure:

Private Function Hyperlink1_onclick() As Boolean
    If MsgBox("Do you really want to jump there?", vbYesNo) = vbYes Then
        Hyperlink1_onclick = True
    End If
End Function

NOTE
Setting the DHTMLEvent.returnValue property to True doesn't work.

The MSHTML library

All DHTML applications include a reference to the MSHTML type library, which contains all the objects that make up the Dynamic HTML object model. You'll probably need some time to get acquainted with this huge library—the version that comes with Internet Explorer 5 includes about 280 classes and interfaces! Its elements also have names that are different from what you might expect. For example, the Window object corresponds to the HTMLWindow2 class, the Document object derives from the HTMLDocument class, the Event object is of class CeventObj, and so on. I don't have enough room to describe all the classes and their properties, methods, and events here, so I can only suggest that you to spend some time with the Object Browser to see the most relevant features of each object.

DHTML Applications

When programming Visual Basic 6's DHTML applications, you have to solve a new class of problems. In this section, I illustrates a few of them.

Navigating to other pages

You can let the user navigate to other pages by simply placing one or more hyperlinks on the page and carefully preventing any of the hyperlinks from returning False in their onclick event procedures. If you're building the target URL in a dynamic way, however, you can't assign it to the <HREF> tag of a hyperlink at design time, and you need to follow one of the following methods:

Whatever method you choose, you should pay attention to how you use relative and absolute paths. In general, all the references to other pages in your application—whether or not they're associated with a DHTMLPage designer—should be relative so that you can easily deploy all the pages of your application to a new Web site without having to recompile the source code. Conversely, all the references to pages outside your Web site should be absolute and be preceded by the http:// prefix.

Loading a page asynchronously

The first time a DHTMLPage is referenced in code, it fires an Initialize event. You should use this element exclusively to initialize local variables. Because all the page elements haven't been created yet, an error occurs if you reference them.

By default, a DHTLMPage object becomes active when the page has been completely downloaded from the Web server. At this point, this object fires the Load event. Because all the elements now exist, you can reference them without any problem. The problem with this simple approach, however, is that the download phase of complex pages with several objects in them—large images, for example—can take a long time to complete. Until the page has been completely downloaded, users are locked out because the controls on the page won't react to their actions.

You can activate asynchronous download by setting the DHTMLPage object's AsyncLoad property to True. In this situation, the Load event fires when the download phase begins and not all the elements on the page have been downloaded yet. This means that you might reference a page element before it's available, which would result in an error. Here are a few techniques that you can use when you turn on the asynchronous loading feature:

Most of the time, you'll have to mix all three techniques. For example, when the page is loaded asynchronously, don't execute critical code in the DHTMLPage_Load event but move it to the Document_onreadystatechange event instead:

Private Sub Document_onreadystatechange()
    If Document.readyState = "complete" Then
        ' Here you can safely access all the elements in the page.
    End If
End Sub

If you can't wait for the onreadystatechange event, you must protect your code from unanticipated errors that would occur when a user tries to access a nonexistent object, or you can use the following routine:

' A reusable function that checks whether an element is available
Function IsAvailable(ByVal id As String) As Boolean
    On Error Resume Next
    id = Document.All(id).id
    IsAvailable = (Err = 0)
End Function

For example, a click on the MainMenu element should be ignored until the menu items are ready:

Private Function MainMenu_onclick() As Boolean
    If Not IsAvailable("MenuItem1") Then Exit Function
    ' Don't execute this code if the menu items aren't available yet.
    ...
End Function

Managing the state

DHTML applications are different from regular Visual Basic applications for an important reason: Because the user is free to navigate from one page to another page—including pages for which you don't provide a hyperlink—you can't be certain about the order in which pages will be visited. This situation contrasts with the usual Visual Basic programming model, which let's you decide which forms can be visited at a given moment.

Another key difference between DHTML and Visual Basic applications is that Internet applications are stateless, in the sense that the HTTP protocol doesn't store any information between requests; it's up to you to maintain the state, if necessary. You can do this using the PutProperty and GetProperty routines that you find in the modDHTML.Bas module included in the DHTML Application template project. This is the source code of the two routines, after stripping out some comment lines:

Sub PutProperty(objDocument As HTMLDocument, strName As String, _
    vntValue As Variant, Optional Expires As Date)
    objDocument.cookie = strName & "=" & CStr(vntValue) & _
        IIf(CLng(Expires) = 0, "", "; expires=" & _
        Format(CStr(Expires), "ddd, dd-mmm-yy hh:mm:ss") & " GMT") 
End Sub

Function GetProperty(objDocument As HTMLDocument, strName As String) _
    As Variant
    Dim aryCookies() As String
    Dim strCookie As Variant
    On Local Error GoTo NextCookie

    ' Split the document cookie object into an array of cookies.
    aryCookies = Split(objDocument.cookie, ";")
    For Each strCookie In aryCookies
        If Trim(VBA.Left(strCookie, InStr(strCookie, "=") - 1)) = _
            Trim(strName) Then
            GetProperty = Trim(Mid(strCookie, InStr(strCookie, "=") + 1))
            Exit Function
        End If
NextCookie:
        Err = 0
    Next strCookie
End Function

As you see, both routines are nothing more than an interface to the Document object's cookie property, so you can directly access this property from your code for some special tasks (for example, to enumerate all the defined cookies). To save a value in a persistent way, call the PutProperty routine:

' Store the name of the user in the "UserName" cookie.
PutProperty Document, "UserName", txtUserName.Value

You can also set an expiration date for the cookie, for example:

' The user password is valid for one week.
PutProperty Document, "UserPwd", txtPassword.Value, Now() + 7

If you don't set an expiration date, the cookie is automatically deleted at the end of the session, when the browser is closed. You can retrieve a cookie using the GetProperty function:

' This returns an empty string if the cookie doesn't exist.
txtUserName.Value = GetProperty(Document, "UserName")

The sample application PropBag.vbp on the Visual Basic CD demonstrates how you can use these routines to pass data between two pages in your project.

NOTE
The PropBag.vbp demo project raises an error when you run it on a system on which Internet Explorer 5 is installed. The error is caused by slight differences in the browser object model. You can fix it by substituting WindowBase.Document with just Document in the code that calls the PutProperty and GetProperty routines. I'm testing this with a late beta of Internet Explorer 5, so it's possible that the error will disappear in the official release.

You typically save a page's state in the Unload event. Don't wait until the Terminate event because when this event fires the page has been already destroyed, and you can't reference its elements any longer. This is similar to the situation you have in the Initialize event.

One last note: The PropBag.vbp demo application might make you believe that you need a cookie any time you're passing data between two pages of your DHTML application, but it isn't strictly necessary. In fact, when you're directly calling another page of your application—using one of the methods outlined in the "Navigating to Other Pages" section, earlier in this chapter—you just need to store the value in a global variable of your ActiveX DLL project. You actually need to resort to a cookie (directly, or indirectly through the routines in the modDHTML.Bas module) only if you want to make some data available to another page that you aren't calling directly or if you want to preserve data among subsequent sessions. (In this latter case, you should specify a suitable value for the Expires argument of the PutProperty routine.)

Creating elements

While you're programming in Visual Basic, you shouldn't forget that you can leverage all the power of Dynamic HTML. To give you an idea of what you can do with Visual Basic and DHTML together in the same application, I'll show you how you can use Visual Basic to query an ADO data source and then dynamically build a table of results right in the browser using the many HTML methods that modify the contents of a page already loaded in the browser. (See the "Text Properties and Methods" section, earlier in this chapter.)

When you plan to fill a portion of the page at run time, for example, with the results of a database query, you need to place a <DIV> section in the proper place. This section should be associated with a nonempty id property so that you can reference it from code. Figure 19-11 shows a typical search page, with two TextBox controls in which the user enters search criteria, and a Search button that starts the search.

Click to view at full size.

Figure 19-11. A simple search page.

The button is followed in the HTML source by an empty (and therefore invisible) <DIV> section whose id is divResults. When the user clicks on the button, the Visual Basic code executes the query and builds an ADO Recordset:

' Edit this constant to match your directory structure.
Const DB_PATH = "C:\Program Files\Microsoft Visual Studio\Vb98\Biblio.mdb"

Private Function cmdSearch_onclick() As Boolean
    Dim rs As New ADODB.Recordset
    Dim conn As String, sql As String
    Dim AuthorSearch As String, TitleSearch As String
    Dim resText As String, recIsOK As Boolean, recCount As Long

    On Error GoTo Error_Handler

    ' Prepare the query string.
    AuthorSearch = txtAuthor.Value
    TitleSearch = txtTitle.Value
    sql = "SELECT Author, Title, [Year Published] AS Year FROM Titles " _
        & "INNER JOIN ([Title Author] INNER JOIN Authors " _
        & "ON [Title Author].Au_ID = Authors.Au_ID) " _
        & "ON Titles.ISBN = [Title Author].ISBN"
    ' You can filter author names right in the SQL query string.
    If Len(AuthorSearch) Then
        sql = sql & " WHERE Author LIKE '" & AuthorSearch & "%'"
    End If
    ' Open the Recordset.
    conn = "Provider=Microsoft.Jet.OLEDB.3.51;Data Source=" & DB_PATH
    rs.Open sql, conn, adOpenStatic, adLockReadOnly

At this point, you start to build a table, with a header row that displays the names of the fields:

    ' Prepare the header of the table.
    resText = "<TABLE BORDER>" _
        & "<TR ALIGN=left>" _
        & "<TH WIDTH=150>Author</TH>" _
        & "<TH WIDTH=300>Title</TH>" _
        & "<TH WIDTH=80>Year</TH>" _
        & "</TR>" & vbCrLf

You can loop through the Recordset and filter out all the records that don't contain the specified string in the Title field (if the user actually entered some text in the txtTitle control). For each record that matches the criteria, this code adds a row to the table:

    Do Until rs.EOF
        recIsOK = True
        ' Filter out unwanted records.
        If Len(TitleSearch) Then
            If InStr(1, rs("Title"), TitleSearch, vbTextCompare) = 0 Then 
                recIsOK = False
            End If
        End If
        ' If the record meets the search criteria, add it to the page.
        If recIsOK Then
            recCount = recCount + 1
            resText = resText & "<TR>" _
                & "<TD>" & rs("Author") & "</TD>" _
                & "<TD>" & rs("Title") & "</TD>" _
                & "<TD>" & rs("Year") & "</TD>" _
                & "</TR>" & vbCrLf
        End If
        rs.MoveNext
    Loop
    rs.Close

When the Recordset has been completely processed, you need simply to append a </TABLE> tag and prepare a simple message that informs about the number of records found. This is the remaining part of the routine:

    If recCount = 0 Then
        ' If no record matched the search criteria, drop the table.
        resText = "<I>No record matches the search criteria</I>"
    Else
        ' Otherwise add the number of found records and complete the table.
        resText = "Found " & recCount & IIf(recCount = 1, _
            " record", " records") & ".<P>" & vbCrLf & resText _
            & "</TABLE>" & vbCrLf
    End If
    ' Substitute the current contents of the divResults section.
    divResults.innerHTML = resText
    Exit Function
    
Error_Handler:
    MsgBox "Error #" & Err.Number & vbCr & Err.Description, vbCritical
End Function

Figure 19-12 shows the program in action, after a query has been successfully completed. You can refine this first version in countless ways, for example, by adding a maximum number of returned records or by creating Next and Previous buttons to let the user navigate through pages of results. (Here's some advice: Prepare Next and Previous buttons on the page and make them visible when needed.)

Click to view at full size.

Figure 19-12. The result of a successful database search.

A problem that you must solve when dynamically adding new controls (as opposed to just plain text elements) is how to reference them in code and trap their events. As an example, I'll show you how you can add two controls at the right of each element in the result table: a CheckBox control that lets the user add that particular title to the order, and a Button control that lets him or her ask for additional details, such as the image of the cover, the table of contents, and so on.

Dynamically creating the controls while the code is building the table isn't difficult, and in fact you only have to ensure that each new control is assigned a unique value for its id property. You must assign this id in order to later get a reference to the control. Here's the code that adds one table row for each record that meets the search criteria (added lines are in boldface):

recCount = recCount + 1
bookmarks(recCount) = rs.Bookmark
resText = resText & "<TR>" _
    & "<TD>" & rs("Author") & "</TD>" _
    & "<TD>" & rs("Title") & "</TD>" _
    & "<TD>" & rs("Year") & "</TD>" _
    & "<TD><INPUT TYPE=BUTTON ID=cmdDetails" & Trim$(recCount) _
    & " VALUE=""Details""></TD>" _
    & "<TD><INPUT TYPE=Checkbox ID=Buy" & Trim$(recCount) _
    & " NAME=Buy?></TD>" _
    & "</TR>" & vbCrLf

The bookmarks array holds the bookmarks for all the records that meet the search criteria; it's defined as a module-level variable, so it's accessible from any routine in the DHTMLPage module.

The next step is to trap the onclick event from the Detail buttons, which at first seems impossible because you've created the buttons dynamically and no code exists for them in the DHTMLPage designer. Fortunately, thanks to event bubbling you just need to trap the onclick event for the Document object and check whether the event comes from one of the controls you've added dynamically:

Private Function Document_onclick() As Boolean
    Dim index As Long, text As String
    ' Not all the elements support the Name or ID property.
    On Error GoTo Error_Handler
    ' Check the ID of the element that fired the event.
    If InStr(DHTMLEvent.srcElement.id, "cmdDetails") = 1 Then
        ' Retrieve the index of the button.
        index = CLng(Mid$(DHTMLEvent.srcElement.id, 11))
        ' Move the Recordset's pointer to that element.
        rs.Bookmark = bookmarks(index)
        ' Show the title of the selected book. (This is just a demo!)
        MsgBox "You requested details for title " & rs("Title")
    Else
        ' Return True to enable the default action of Checkbox controls.
        Document_onclick = True
    End If
End Function

Notice how you can test whether the onclick event was raised by one of the Detail buttons and how you extract the index of the control.

Your next task is to prepare a list of all the titles that have been flagged for ordering, which you accomplish using the following piece of code:

Dim text As String
For index = 1 To UBound(bookmarks)
    If Document.All("Buy" & Trim$(index)).Checked Then
        rs.Bookmark = bookmarks(index)
        text = text & rs("Title") & vbCr
    End If
Next
If Len(text) Then
    text = "Confirm the order for the following title(s)" & vbCr & text
    If MsgBox(text, vbYesNo + vbExclamation) = vbYes Then
        ' In a real application, you would insert the code that processes
        ' the order right here.
        MsgBox "Order filed!", vbInformation
    Else
        MsgBox "Order canceled!", vbCritical
    End If
End If

For more information, see the demonstration application provided on the companion CD. The project includes two distinct DHTMLPage modules: One does a simple search, and the other builds a more complex page with Button and CheckBox controls inside the grid. (See Figure 19-13.) Select the page to run in the Debugging tab of the Project Properties dialog box. I explain how to do this in the next section.

Click to view at full size.

Figure 19-13. A DHTML page that dynamically builds its own array of controls.

Testing DHTML applications

The beauty of DHTML applications is that you can test your code inside the IDE, using all the tools that make debugging Visual Basic applications a relatively easy job. You're so used to such debugging features that you've probably missed a rather important point: You're executing your DHTML application inside the environment but Internet Explorer is behaving as if you compiled your code to an ActiveX DLL that runs inside the Explorer's address space. This little magic is made possible by the VB6Debug DLL, a file that you find in the main Visual Basic installation directory. Be careful not to delete it, or you won't be able to do such cross-process debugging any longer.

When you test a DHTML application, you can take advantage of all the options you find in the Debugging tab of the Project Properties dialog box, shown in Figure 19-14. This tab is new to Visual Basic 6 and is disabled in Standard EXE projects because it's useful only when you're developing ActiveX components intended for consumption by client programs such as Internet Explorer. The options this tab offers (which I'll describe shortly) greatly simplify the testing of such components because they let you automatically start the client application that uses them. You can choose one of four different actions when the current project starts its execution inside the environment:

Wait For Components to be Created This is the default action: The Visual Basic IDE silently waits until the client application asks the COM subsystem to create the component.

Start Component You start one of the components defined in the current project and let it decide what to do. The default behavior for DHTMLPage designers is to load theHTM source file into Internet Explorer so that the component is automatically activated immediately afterward. If you select a UserControl or a UserDocument, Visual Basic creates a temporaryHTM page that contains a reference to it and then loads the page into the browser; this option lets you test how the control behaves in an HTML page. The component you select in this combo box control doesn't interfere with the selection you make in the Startup Object combo box in the General tab of the same dialog box. For example, you can select a DHTMLPage designer as a Start Component and still have the Sub Main procedure automatically execute when the component is instantiated.

Start Program This option lets you specify the path of the executable to launch when you run the project. Select this option when you know that the selected program will in turn create an instance of the component being developed. For example, you can create another application in Visual Basic that creates an instance of the component under development.

Start Browser with URL You can start the default browser and load an HTML page in it. This option enables you to test an ActiveX Control or DLL referenced in an existingHTM page. as opposed to the blank temporary page that Visual Basic automatically creates when you select the Start Component option.

The page also contains a check box that you can tick if you want to use the existing instance of the browser (if one is already running), or clear if you want to start a new instance each time you run the project.

To have Internet Explorer automatically create an instance of the ActiveX DLL that's being developed in the IDE, Visual Basic adds an <OBJECT> tag at the beginning of the HTM page that contains all the elements defined in the DHTMLPage designer:

<OBJECT 
ID="DHTMLPage1" CLASSID="clsid:8F0A368F-C5BC-11D2-BAC5-0080C8F21830" 
WIDTH=0 HEIGHT=0></OBJECT>

Click to view at full size.

Figure 19-14. The Debugging tab of the Project Properties dialog box.

Deploying a DHTML application

Once you've thoroughly tested your DHTML application, you must prepare a distribution package for it. This package comprises the following elements:

You create the distribution package using the Package and Deployment Wizard, which you can run as a Visual Basic add-in or as a stand-alone program. This is the sequence of actions you should perform:

  1. In the topmost field in the Package and Deployment Wizard, select the DHTML project, and click on the Package button. The wizard asks whether you want to recompile the project if it finds that the DLL file is older than any of the source code files.
  2. In the Select Type page, select the Internet Package type and click Next.
  3. In the Package Folder page, enter the path of a directory in which you want the wizard to place the distribution package.
  4. In the Included Files page, you'll see a list of all the files that make up the application, including Visual Basic and OLE Automation libraries but excluding .hml files and data files needed by the application.
  5. In the File Source page (see Figure 19-15), specify the site from which each file should be downloaded. By default, all the Visual Basic, ADO, and other system files are downloaded from the Microsoft Web site, which is often the best choice.
  6. Click to view at full size.

    Figure 19-15. The File Source page of the Package and Deployment Wizard.

  7. In the Safety Setting tab, you decide whether the components included in the DLL are Safe For Scripting and Safe For Initialization. (For more information about these terms, see the section "Component Download" in Chapter 17.)
  8. In the last page of the wizard, you can assign a name to the current script so that you can easily repeat these steps in the future.

The wizard creates a new directory and puts in it a CAB file (which contains the DLL) and all the HTM files belonging to your application. You now need to deploy these files to a Web server, and you can use the Package and Deployment Wizard to achieve this:

  1. Click on the Deploy button and select the script name you entered in step 7 of the previous sequence.
  2. In the Deployment Method page, select the Web Publishing option.
  3. In the Items To Deploy page, select which files should be deployed. The first time you run the wizard, you normally deploy all the files except those that are on the Microsoft Web site, but in subsequent deployment operations you can omit the files that haven't changed in the meantime.
  4. In the Additional Items To Deploy page, you can select files and entire folders for deployment. Here you select all the ancillary files, such as images, data files, WAV files, and so on.
  5. In the Web Publishing Site page (see Figure 19-16), you must enter the complete URL of the site to which items should be deployed (for example http://www.yoursite.com).You also enter the Web publishing protocol to be used (FTP or HTTP Post). Tick the Unpack And Install Server-Side Cab option if you want the CAB file be unpacked after deployment. When you press the Next button, the wizard asks whether you want to save information about this site in the Registry.
  6. Click to view at full size.

    Figure 19-16. The Web Publishing Site page of the Package and Deployment Wizard.

  7. In the last page of the wizard, you can give a name to this deployment script and click the Finish button to initiate the deployment phase.

When the deployment is complete, uninstall the ActiveX DLL from your system, and then use your browser to navigate to the main HTM page of the application. If everything is OK, the browser should download the CAB file, install the DLL, and start your compiled DHTML application. The browser knows from what site the DLL can be downloaded because the Package and Deployment wizard has patched the <OBJECT> tags inside all theHTM pages with a CODEBASE attribute. (The text added by the wizard is in boldface.)

<OBJECT CODEBASE=Search.CAB#Version1,0,0,0 
ID="DHTMLPage2" CLASSID="clsid:8F0A368F-C5BC-11D2-BAC5-0080C8F21830" 
WIDTH=0 HEIGHT=0></OBJECT>


As you can see in the preceding HTML fragment, the Package and Deployment Wizard produces an incorrect CODEBASE attribute; the version number should be preceded by an equal sign. So you need to manually edit it, like this:

<OBJECT CODEBASE=Search.CAB#Version=1,0,0,0

Troubleshooting

I conclude this section with a few tips for building better DHTML applications: